{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- https://course.rs/test/intro.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 编写测试及控制执行\n",
    "\n",
    "- https://course.rs/test/write-tests.html\n",
    "\n",
    "**tl;dr**\n",
    "\n",
    "- 使用 `#[test]` 标注函数即可编写单元测试；测试通常包含初始化、执行、断言三步。\n",
    "- `cargo test` 会自动发现并并行运行测试（可用 `-- --test-threads=1` 顺序运行）。\n",
    "- 常用断言宏：`assert!`、`assert_eq!` 等；可自定义错误信息，也能测试预期的 `panic`（`#[should_panic]` / `expected = \"...\"`）。\n",
    "- 测试函数可以返回 `Result` 以便使用 `?`；此模式下不能配合 `#[should_panic]`。\n",
    "- 通过 `cargo test <过滤词>` 仅运行部分测试；`#[ignore]` 可跳过耗时用例，可用 `-- --ignored` 单独运行。\n",
    "- `cargo test -- --show-output` 查看通过用例的 `println!` 输出。\n",
    "- 支持组合过滤（名称筛选 + 忽略标记）；doc tests 会自动执行。\n",
    "- `dev-dependencies`（如 `pretty_assertions`）仅在测试/开发环境使用。\n",
    "- `cargo test --no-run` 只编译测试；编译后的测试可执行文件位于 `target/debug/deps/`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 Rust 中，测试是通过函数的方式实现的，它可以用于验证被测试代码的正确性。测试函数往往依次执行以下三种行为：\n",
    "\n",
    "1. 设置所需的数据或状态\n",
    "2. 运行想要测试的代码\n",
    "3. 判断( assert )返回的结果是否符合预期\n",
    "\n",
    "让我们来看看该如何使用 Rust 提供的特性来按照上述步骤编写测试用例。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [测试函数](https://course.rs/test/write-tests.html#测试函数)\n",
    "\n",
    "当使用 `Cargo` 创建一个 `lib` 类型的包时，它会为我们自动生成一个测试模块。先来创建一个 `lib` 类型的 `adder` 包："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "    Creating library `adder` package\n",
       "note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1 && cargo new adder --lib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "pub fn add(left: u64, right: u64) -> u64 {\n",
       "    left + right\n",
       "}\n",
       "\n",
       "#[cfg(test)]\n",
       "mod tests {\n",
       "    use super::*;\n",
       "\n",
       "    #[test]\n",
       "    fn it_works() {\n",
       "        let result = add(2, 2);\n",
       "        assert_eq!(result, 4);\n",
       "    }\n",
       "}\n"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1/adder && cat ./src/lib.rs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "其中，`tests` 就是一个测试模块，`it_works` 则是我们的主角：测试函数。\n",
    "\n",
    "可以看出，测试函数需要使用 `test` 属性进行标注。关于属性( `attribute` )，我们在之前的章节已经见过类似的 `derive`，使用它可以派生自动实现的 `Debug` 、`Copy` 等特征，同样的，使用 `test` 属性，我们也可以获取 Rust 提供的测试特性。\n",
    "\n",
    "经过 `test` 标记的函数就可以被测试执行器发现，并进行运行。当然，在测试模块 `tests` 中，还可以定义非测试函数，这些函数可以用于设置环境或执行一些通用操作：例如为部分测试函数提供某个通用的功能，这种功能就可以抽象为一个非测试函数。\n",
    "\n",
    "换而言之，正是因为测试模块既可以定义测试函数又可以定义非测试函数，导致了我们必须提供一个特殊的标记 `test`，用于告知哪个函数才是测试函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### [assert_eq](https://course.rs/test/write-tests.html#assert_eq)\n",
    "\n",
    "在测试函数中，还使用到了一个内置的断言：`assert_eq`，该宏用于对结果进行断言：`2 + 2` 是否等于 `4`。与之类似，Rust 还内置了其它一些实用的断言，具体参见[后续章节](https://course.rs/test/assertion.html)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [cargo test](https://course.rs/test/write-tests.html#cargo-test)\n",
    "\n",
    "下面使用 `cargo test` 命令来运行项目中的所有测试:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "running 1 test\n",
       "test tests::it_works ... ok\n",
       "\n",
       "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n",
       "\n",
       "\n",
       "running 0 tests\n",
       "\n",
       "test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n",
       "\n",
       "   Compiling adder v0.1.0 (/mnt/d/dev_learn/learnrustbyai_fk/rust_course/08_test/8.1/adder)\n",
       "    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.54s\n",
       "     Running unittests src/lib.rs (target/debug/deps/adder-735a0a313451494b)\n",
       "   Doc-tests adder\n"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1/adder && cargo test"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面测试输出中，有几点值得注意:\n",
    "\n",
    "- 测试用例是分批执行的，`running 1 test` 表示下面的输出 `test result` 来自一个测试用例的运行结果。\n",
    "- `test tests::it_works` 中包含了测试用例的名称\n",
    "- `test result: ok` 中的 `ok` 表示测试成功通过\n",
    "- `1 passed` 代表成功通过一个测试用例(因为只有一个)，`0 failed` : 没有测试用例失败，`0 ignored` 说明我们没有将任何测试函数标记为运行时可忽略，`0 filtered` 意味着没有对测试结果做任何过滤，`0 measured` 代表[基准测试(benchmark)](https://course.rs/test/benchmark.html)的结果\n",
    "\n",
    "关于 `filtered` 和 `ignored` 的使用，在本章节的后续内容我们会讲到，这里暂且略过。\n",
    "\n",
    "还有一个很重要的点，输出中的 `Doc-tests adder` 代表了文档测试，由于我们的代码中没有任何文档测试的内容，因此这里的测试用例数为 `0`，关于文档测试的详细介绍请参见[这里](https://course.rs/basic/comment.html#文档注释)。\n",
    "\n",
    "大家还可以尝试修改下测试函数的名称，例如修改为 `exploration`，看看运行结果将如何变化。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### [失败的测试用例](https://course.rs/test/write-tests.html#失败的测试用例)\n",
    "\n",
    "是时候开始写自己的测试函数了，为了演示，这次我们来写一个会运行失败的:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "pub fn add(left: u64, right: u64) -> u64 {\n",
       "    left + right\n",
       "}\n",
       "\n",
       "#[cfg(test)]\n",
       "mod tests {\n",
       "    use super::*;\n",
       "\n",
       "    #[test]\n",
       "    fn it_works() {\n",
       "        let result = add(2, 2);\n",
       "        assert_eq!(result, 4);\n",
       "    }\n",
       "\n",
       "    #[test]\n",
       "    fn another() {\n",
       "        panic!(\"Make this test fail\");\n",
       "    }\n",
       "}"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1/adder_fail && cat ./src/lib.rs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [],
   "source": [
    "! cd 8.1/adder_fail && cargo test -p adder --lib"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从结果看，两个测试函数，一个成功，一个失败，同时在输出中准确的告知了失败的函数名: `failures: tests::another`，同时还给出了具体的失败原因： `tests::another stdout`。这两者虽然看起来存在重复，但是前者用于说明每个失败的具体原因，后者用于给出一眼可得结论的汇总信息。\n",
    "\n",
    "有同学可能会好奇，这两个测试函数以什么方式运行？ 它们会运行在同一个线程中吗？答案是否定的，Rust 在默认情况下会为每一个测试函数启动单独的线程去处理，当主线程 `main` 发现有一个测试线程死掉时，`main` 会将相应的测试标记为失败。\n",
    "\n",
    "事实上，多线程运行测试虽然性能高，但是存在数据竞争的风险，在后文我们会对其进行详细介绍并给出解决方案。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [自定义失败信息](https://course.rs/test/write-tests.html#自定义失败信息)\n",
    "\n",
    "默认的失败信息在有时候并不是我们想要的，来看一个例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "    Creating library `greeting` package\n",
       "note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1 && cargo new greeting --lib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [],
   "source": [
    "! cd 8.1/greeting && cat ./src/lib.rs"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "! cd 8.1/greeting && cargo test\n",
    "\n",
    "running 1 test\n",
    "test tests::greeting_contains_name ... FAILED\n",
    "\n",
    "failures:\n",
    "\n",
    "---- tests::greeting_contains_name stdout ----\n",
    "\n",
    "thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:\n",
    "assertion failed: result.contains(\"孙飞\")\n",
    "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n",
    "\n",
    "\n",
    "failures:\n",
    "    tests::greeting_contains_name\n",
    "\n",
    "test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n",
    "\n",
    "error: test failed, to rerun pass `--lib`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "vscode": {
     "languageId": "rust"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "pub fn greeting(name: &str) -> String {\n",
       "    format!(\"Hello {}!\", name)\n",
       "}\n",
       "\n",
       "#[cfg(test)]\n",
       "mod tests {\n",
       "    use super::*;\n",
       "\n",
       "    #[test]\n",
       "    fn greeting_contains_name() {\n",
       "        let result = greeting(\"Sunface\");\n",
       "        let target = \"孙飞\";\n",
       "        assert!(\n",
       "            result.contains(target),\n",
       "            \"你的问候中并没有包含目标姓名 {} ，你的问候是 `{}`\",\n",
       "            target,\n",
       "            result\n",
       "        );\n",
       "    }\n",
       "}"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "! cd 8.1/greeting_opt && cat ./src/lib.rs"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "! cd 8.1/greeting_opt && cargo test\n",
    "\n",
    "running 1 test\n",
    "test tests::greeting_contains_name ... FAILED\n",
    "\n",
    "failures:\n",
    "\n",
    "---- tests::greeting_contains_name stdout ----\n",
    "\n",
    "thread 'tests::greeting_contains_name' panicked at src/lib.rs:13:9:\n",
    "你的问候中并没有包含目标姓名 孙飞 ，你的问候是 `Hello Sunface!`\n",
    "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n",
    "\n",
    "\n",
    "failures:\n",
    "    tests::greeting_contains_name\n",
    "\n",
    "test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n",
    "\n",
    "error: test failed, to rerun pass `--lib`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [测试 panic](https://course.rs/test/write-tests.html#测试-panic)\n",
    "\n",
    "在之前的例子中，我们通过 `panic` 来触发报错，但是如果一个函数本来就会 `panic` ，而我们想要检查这种结果呢？\n",
    "\n",
    "也就是说，我们需要一个办法来测试一个函数是否会 `panic`，对此， Rust 提供了 `should_panic` 属性注解，和 `test` 注解一样，对目标测试函数进行标注即可："
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Rust",
   "language": "rust",
   "name": "rust"
  },
  "language_info": {
   "codemirror_mode": "rust",
   "file_extension": ".rs",
   "mimetype": "text/rust",
   "name": "Rust",
   "pygment_lexer": "rust",
   "version": ""
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
